/*
 * Copyright © 2023 广州宏天软件股份有限公司 hotent
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.hotent.project.auth.service.impl;

import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.hotent.base.conf.JwtConfig;
import com.hotent.base.conf.SaaSConfig;
import com.hotent.base.conf.SsoConfig;
import com.hotent.base.enums.ResponseErrorEnums;
import com.hotent.base.exception.BaseException;
import com.hotent.base.exception.CertificateException;
import com.hotent.base.exception.ServerRejectException;
import com.hotent.base.feign.PortalFeignService;
import com.hotent.base.feign.UCFeignService;
import com.hotent.base.jwt.JwtAuthenticationRequest;
import com.hotent.base.jwt.JwtAuthenticationResponse;
import com.hotent.base.jwt.JwtTokenHandler;
import com.hotent.base.manager.CaptchaManager;
import com.hotent.base.model.CommonResult;
import com.hotent.base.service.TwoVerifyService;
import com.hotent.base.util.*;
import com.hotent.project.auth.service.AuthenticationService;
import com.hotent.uc.api.model.IUser;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.entity.ContentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.sql.SQLSyntaxErrorException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 用户认证 服务实现类
 *
 * @author 欧阳高龙
 * @company 广州宏天软件股份有限公司
 * @since 2022/9/28 14:27
 */
@Service

public class AuthenticationServiceImpl implements AuthenticationService {

    private static final Logger logger = LoggerFactory.getLogger(AuthenticationServiceImpl.class);

    public static final String MODE_SSO = "sso";

    @Resource
    AuthenticationManager authenticationManager;
    @Resource
    JwtTokenHandler jwtTokenHandler;
    @Resource
    UserDetailsService userDetailsService;
    @Resource
    SsoConfig ssoConfig;
    @Value("${sso.sso.ticket:'gzvtczdpk'}")
    private String ticket;
    @Value("${system.mode.demo:false}")
    protected boolean demoMode;
    @Resource
    UCFeignService uCFeignService;
    @Resource
    PortalFeignService portalFeignService;
    @Resource
    UCFeignService ucFeignService;
    @Resource
    SaaSConfig saasConfig;
    @Resource
    JwtConfig jwtConfig;
    @Resource
    private CaptchaManager captchaManager;
    @Resource
    private TwoVerifyService twoVerifyService;

    @Override
    public ResponseEntity<?> createAuthenticationToken(JwtAuthenticationRequest authenticationRequest)
        throws AuthenticationException, CertificateException, InvalidKeySpecException, NoSuchAlgorithmException {

        if (!captchaManager.verifyCode(authenticationRequest)) {
            return ResponseEntity.ok(CommonResult.result(ResponseErrorEnums.BAD_CAPTCHA));
        }
        String reqAccount = authenticationRequest.getUsername();
        String reqPassword = "";
        // 清除用户缓存
        CacheEvictUtil.deleteUserDetailsCache(reqAccount);
        try {
            // 密码rsa解密
            reqPassword = RSAUtil.Decrypt(authenticationRequest.getPassword());
        } catch (Exception e) {
            throw new RuntimeException("解密密码异常，请检查RSA公钥和私钥配置");
        }
        String errorMsg = "";
        try {
            authenticate(reqAccount, reqPassword);
        } catch (Exception e) {
            logger.error(String.format("Login failed account[%s].", reqAccount), e);
            errorMsg = "账号或密码错误";
            Throwable cause = ExceptionUtil.getRootCauseOrSelf(e.getCause());
            if (cause instanceof CertificateException) {
                CertificateException ce = (CertificateException)cause;
                errorMsg = ce.getMessage();
            } else if (cause instanceof SQLSyntaxErrorException) {
                SQLSyntaxErrorException sqlSyntaxErrorException = (SQLSyntaxErrorException)cause;
                logger.error(sqlSyntaxErrorException.getMessage());
            } else if (cause instanceof BaseException) {
                BaseException baseException = (BaseException)cause;
                logger.error(baseException.getMessage());
            } else if (e instanceof CertificateException) {
                logger.error(e.getMessage());
            } else if (e instanceof LockedException) {
                logger.error("账号{}被禁用或离职", reqAccount);
                // errorMsg = I18nUtil.handleI18nMessage("account.disabled.exception", "账号被禁用或离职");
            } else if (e instanceof InternalAuthenticationServiceException) {
                logger.error("账户{}错误或该租户未启用", reqAccount);
                // errorMsg = I18nUtil.handleI18nMessage("account.error.exception", "账户错误或该租户未启用");
            }
            if (captchaManager.addFailedCount(authenticationRequest.getUsername())) {
                return ResponseEntity.ok(CommonResult.result(ResponseErrorEnums.BAD_CAPTCHA, errorMsg));
            }
            throw new RuntimeException(errorMsg);
        }
        // 当前切中的方法
        HttpServletRequest request = HttpUtil.getRequest();
        boolean isMobile = HttpUtil.isMobile(request);
        // Reload password post-security so we can generate the token
        final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
        final String token = jwtTokenHandler.generateToken(userDetails);
        String userName = userDetails.getUsername();
        String account = "";
        String userId = "";
        String twoVerifySecret = "";
        boolean loginStatus = true;
        Map<String, Object> userAttrs = new HashMap<String, Object>();
        if (userDetails instanceof IUser) {
            IUser user = ((IUser)userDetails);
            userName = user.getFullname();
            account = user.getAccount();
            userId = user.getUserId();
            request.setAttribute("loginUser", String.format("%s[%s]", userName, account));
            // 校验密码策略
            userAttrs.put("tenantId", user.getTenantId());
            Map userMap = JSONUtil.toBean(JSONUtil.toJsonStr(user), Map.class);
            twoVerifySecret = StringUtil.isNotEmpty(String.valueOf(userMap.get("twoVerifySecret"))) ? userMap.get("twoVerifySecret").toString() : null;
        }
        // 处理单用户登录
        JwtAuthenticationResponse jwtAuthenticationResponse = new JwtAuthenticationResponse(token, userName, account, userId, jwtConfig.getExpirationLong(), loginStatus, userAttrs);
        // 登录成功重置触发验证码校验失败次数
        // captchaManager.resetLimit(reqAccount);
        return ResponseEntity.ok(jwtAuthenticationResponse);
    }

    /**
     * Authenticates the user. If something is wrong, an {@link AuthenticationException} will be thrown
     */
    private void authenticate(String username, String password) throws AuthenticationException, CertificateException {
        Objects.requireNonNull(username);
        Objects.requireNonNull(password);
        authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
    }

    @Override
    public ResponseEntity<?> refreshAndGetAuthenticationToken(HttpServletRequest request) {
        String authToken = request.getHeader(jwtConfig.getHeader());
        final String token = authToken.substring(7);
        String tenantId = jwtTokenHandler.getTenantIdFromToken(token);
        String account = jwtTokenHandler.getUsernameFromToken(token);
        String refreshedToken = jwtTokenHandler.refreshToken(token);
        boolean isMobile = HttpUtil.isMobile(request);
        // 处理单用户登录 更新单用户登录的token
        handleSingleLogin(isMobile, tenantId, account, refreshedToken);
        long expiration = jwtConfig.getExpirationLong();
        return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken, account, account, "", expiration, null));
    }

    /**
     * 处理单用户登录
     * 
     * @param isMobile
     * @param username
     * @param token
     */
    private void handleSingleLogin(boolean isMobile, String tenantId, String username, String token) {
        // 如果是单用户登录
        if (jwtConfig.isSingle()) {
            String userAgent = isMobile ? "mobile" : "pc";
            // 非SaaS模式
            if (StringUtil.isEmpty(tenantId) && !saasConfig.isEnable()) {
                tenantId = "-1";
            }
            // 以当前登录设备、租户ID、用户账号为key将token存放到缓存中
            jwtTokenHandler.putTokenInCache(userAgent, tenantId, username, jwtConfig.getExpiration(), token);
        }
    }

    @Override
    public void signOut(HttpServletRequest request) {
        String authToken = request.getHeader(jwtConfig.getHeader());
        if (StringUtil.isEmpty(authToken)) {
            return;
        }
        final String token = authToken.substring(7);
        String tenantId = jwtTokenHandler.getTenantIdFromToken(token);
        String account = jwtTokenHandler.getUsernameFromToken(token);
        boolean isMobile = HttpUtil.isMobile(request);
        handleLogout(isMobile, tenantId, account);
    }

    /**
     * 处理用户登出
     * 
     * @param tenantId
     * @param account
     */
    private void handleLogout(boolean isMobile, String tenantId, String account) {
        // 如果是单用户登录
        if (jwtConfig.isSingle()) {
            String userAgent = isMobile ? "mobile" : "pc";
            // 非SaaS模式
            if (StringUtil.isEmpty(tenantId) && !saasConfig.isEnable()) {
                tenantId = "-1";
            }
            jwtTokenHandler.removeFromCache(userAgent, tenantId, account);
        }
    }

    @Override
    public ResponseEntity<?> ssoAuth(Optional<String> ticket, Optional<String> code, Optional<String> ssoMode,
        Optional<String> ssoUserId, String service) throws AuthenticationException, ClientProtocolException, IOException {
        Assert.isTrue(ssoConfig.isEnable(), "当前服务未开启单点登录");
        String username = null;
        String mode = ssoConfig.getMode();
        if (ssoMode.isPresent()) {
            mode = ssoMode.get();
        }
        // 使用cas认证
        if (ticket.isPresent() && SsoConfig.MODE_CAS.equals(mode)) {
            username = getUserNameWithCas(ticket.get(), service);
        }
        // 使用oauth认证
        else if (code.isPresent() && SsoConfig.MODE_OAUTH.equals(mode)) {
            username = getUserNameWithOauth(code.get(), service);
        }
        // 使用jwt认证
        else if (ticket.isPresent() && code.isPresent() && SsoConfig.MODE_JWT.equals(mode)) {
            username = jwtTokenHandler.getUsernameFromToken(ticket.get());
        }
        // 使用sso认证
        else if (ticket.isPresent() && ssoUserId.isPresent() && MODE_SSO.equals(mode)) {
            username = getUserNameWithSso(ssoUserId.get(), ticket.get());
        } else {
            throw new ServerRejectException("单点登录模式匹配异常");
        }

        // 清除用户缓存
        CacheEvictUtil.deleteUserDetailsCache(username);

        // 当前切中的方法
        HttpServletRequest request = HttpUtil.getRequest();
        boolean isMobile = HttpUtil.isMobile(request);
        // Reload password post-security so we can generate the token
        final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        final String token = jwtTokenHandler.generateToken(userDetails);
        String userName = userDetails.getUsername();
        String account = "";
        String userId = "";
        Map<String, Object> userAttrs = new HashMap<String, Object>();
        if (userDetails instanceof IUser) {
            IUser user = ((IUser)userDetails);
            userName = user.getFullname();
            account = user.getAccount();
            userId = user.getUserId();
            request.setAttribute("loginUser", String.format("%s[%s]", userName, account));
            userAttrs.put("tenantId", user.getTenantId());
        }
        // 获取超时时间
        logger.debug("通过单点认证登录成功。");
        // 处理单用户登录
        if (!(code.isPresent() && SsoConfig.MODE_JWT.equals(mode))) {
            handleSingleLogin(isMobile, MapUtil.getString(userAttrs, "tenantId"), account, token);
        }
        // Return the token
        return ResponseEntity.ok(new JwtAuthenticationResponse(token, userName, account, userId, jwtConfig.getExpirationLong(), userAttrs));
    }

    private String getUserNameWithCas(String ticket, String service) throws IOException {
        String casUserDetail = "";
        String username = null, errorCode = "";
        try {
            casUserDetail = FluentUtil.get(String.format("%s/p3/serviceValidate?ticket=%s&service=%s", ssoConfig.getCasUrl(), ticket, service), "");
            String json = XmlUtil.toJson(casUserDetail);
            JsonNode jsonNode = JsonUtil.toJsonNode(json);

            if (jsonNode.has("authenticationSuccess")) {
                username = jsonNode.get("authenticationSuccess").get("user").asText();
            } else if (jsonNode.has("authenticationFailure")) {
                errorCode = jsonNode.get("authenticationFailure").get("code").asText();
                throw new RuntimeException(errorCode);
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.info("获取cas认证信息失败：" + casUserDetail);
            throw new RuntimeException("获取cas认证信息失败： " + e.getMessage());
        }
        return username;
    }

    private String getUserNameWithOauth(String code, String service) {
        String userName = null;
        String oauthTokenUrl = ssoConfig.getOauthTokenUrl();
        String stufix = String.format("&code=%s&redirect_uri=%s", code, service);
        try {
            String header = ssoConfig.getOauthBasicHeader();
            String tokenResult = FluentUtil.post(oauthTokenUrl + stufix, header, null, ContentType.APPLICATION_FORM_URLENCODED);
            JsonNode jsonNode = JsonUtil.toJsonNode(tokenResult);
            if (jsonNode != null && jsonNode.isObject()) {
                String token = jsonNode.get(ssoConfig.getOauthAccesstokenKey()).asText();
                String oauthCheckUrl = ssoConfig.getOauthCheckUrl();
                String checkResult = FluentUtil.post(oauthCheckUrl + token, null, null, ContentType.APPLICATION_FORM_URLENCODED);
                JsonNode checkJNode = JsonUtil.toJsonNode(checkResult);
                if (checkJNode != null && checkJNode.isObject()) {
                    userName = checkJNode.get(ssoConfig.getOauthUsernameKey()).asText();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("获取oauth认证信息失败", e);
        }
        return userName;
    }

    private String getUserNameWithSso(String userId, String token) {
        String userName = null;
        try {
            String ssoToken = getToken(userId, ticket);
            if (ssoToken.equals(token)) {
                userName = userId;
            } else {
                throw new RuntimeException("单点登录验证失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("单点登录验证失败", e);
        }
        return userName;
    }

    // 加密方法
    private String getToken(String userId, String ticketKey) throws IOException {
        Date currDate = Calendar.getInstance().getTime();
        DateFormat dateFormatter = new SimpleDateFormat("yyyyMd");
        String dateFormatterStr = dateFormatter.format(currDate);
        DateFormat timeFormatter = new SimpleDateFormat("H:mm");
        String timeFormatterStr = timeFormatter.format(currDate).substring(0, 2); // H:mm

        // 时间截取为前两位，0-9时截取： 0：1：2：----9：，10-24时截取：10，11，12，13，24；
        // 为确保截取正确，执行以下操作
        if ("00".equals(timeFormatterStr)) // 当为“00”时，改为“0：”
            timeFormatterStr = "0:";
        // 当为01-09时，把前面的“0”去掉，然后添加“：”
        if (!timeFormatterStr.contains(":") && timeFormatterStr.contains("0") && !"10".equals(timeFormatterStr) && !"20".equals(timeFormatterStr))
            timeFormatterStr = timeFormatterStr.replace("0", "") + ":";

        // 加密格式 == 201829
        String code = dateFormatterStr + userId + timeFormatterStr + ticketKey;// 读配置文件ticket,双方约定密钥
        return getDigestStr(code);
    }

    private String getDigestStr(String info) {
        try {
            byte[] res = info.getBytes();
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] result = md.digest(res);
            for (int i = 0; i < result.length; i++) {
                md.update(result[i]);
            }
            byte[] hash = md.digest();
            StringBuffer d = new StringBuffer("");
            for (int i = 0; i < hash.length; i++) {
                int v = hash[i] & 0xFF;
                if (v < 16) {
                    d.append("0");
                }
                d.append(Integer.toString(v, 16).toUpperCase());
            }
            return d.toString();
        } catch (Exception e) {
            return null;
        }
    }

}
